package org.odst.transformer;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.odftoolkit.odfdom.doc.table.OdfTableCell;
import org.odftoolkit.odfdom.doc.table.OdfTableRow;
import org.odftoolkit.odfdom.dom.OdfNamespaceNames;
import org.odst.controller.SheetTransformationController;
import org.odst.controller.SheetTransformationControllerImpl;
import org.odst.controller.WorkbookTransformationController;
import org.odst.exception.ParsePropertyException;
import org.odst.formula.ListRange;
import org.odst.parser.Cell;
import org.odst.parser.CellParser;
import org.odst.processor.CellProcessor;
import org.odst.processor.RowProcessor;
import org.odst.tag.Block;
import org.odst.transformation.ResultTransformation;
import org.odst.util.SheetHelper;
import org.w3c.dom.Node;

/**
 * @author Leonid Vysochyn
 * @author Cyril PODER
 */
public class SheetTransformer {
    protected final Log log = LogFactory.getLog(getClass());

    /**
     * {@link java.util.Set} of all collections to outline
     */
    private Set<String> groupedCollections = new HashSet<String>();

    /**
     * Stores the names of all 'fixed size' collections.
     * 'Fixed size' collection is a collection with fixed number of items which do not require to create new rows in excel file
     * because all rows for them are already presented in template file.
     */
    private Set<String> fixedSizeCollections = new HashSet<String>();

    /**
     * {@link org.odst.transformer.Configuration} class
     */
    private Configuration configuration;

    private List<RowProcessor> rowProcessors = new ArrayList<RowProcessor>();

    private List<RowTransformer> ownTransformers = new ArrayList<RowTransformer>();


    /**
     * {@link org.odst.processor.CellProcessor} cell processors
     */
    private List<CellProcessor> cellProcessors = new ArrayList<CellProcessor>();

    public SheetTransformer(Set<String> fixedSizeCollections, Set<String> groupedCollections, List<RowProcessor> rowProcessors, List<CellProcessor> cellProcessors, Configuration configuration) {
        this.fixedSizeCollections = fixedSizeCollections;
        this.groupedCollections = groupedCollections;
        this.rowProcessors = rowProcessors;
        this.cellProcessors = cellProcessors;
        this.configuration = configuration;
    }

    public SheetTransformer(Set<String> fixedSizeCollections, Set<String> groupedCollections, List<RowProcessor> rowProcessors, List<CellProcessor> cellProcessors) {
        this.fixedSizeCollections = fixedSizeCollections;
        this.groupedCollections = groupedCollections;
        this.rowProcessors = rowProcessors;
        this.cellProcessors = cellProcessors;
        this.configuration = new Configuration();
    }

    public void setGroupedCollections(Set<String> groupedCollections) {
        this.groupedCollections = groupedCollections;
    }

    void transformSheet(WorkbookTransformationController workbookTransformationController, Sheet sheet) throws ParsePropertyException {
        exposePOIBeans(sheet);
        if (!sheet.getBeanParams().isEmpty()) {
            SheetTransformationController stc = new SheetTransformationControllerImpl(sheet);
            workbookTransformationController.addSheetTransformationController(stc);
            OdfTableRow odfRow = sheet.getOooSheet().getRowByIndex(0);
            while(odfRow != null) {
                List<RowTransformer> rowTransformers = parseRow(sheet, odfRow);
                if (!rowTransformers.isEmpty()) {
                    // process first Transformer
                    ResultTransformation processResult = new ResultTransformation();
                    RowTransformer rowTransformer = (RowTransformer) rowTransformers.get(0);
                    if (rowTransformer != null) {
                        Row row = rowTransformer.getRow();
                        applyRowProcessors(sheet, row);
                        processResult = rowTransformer.transform(stc, this/*, null*/);
                        ownTransformers.add(rowTransformer);
                        if (!processResult.isTagProcessResult()) {
                        	odfRow = sheet.getOooSheet().getRowByIndex(row.getRowIndex() + processResult.getNextRowShift() + 1);
                        } else if (processResult.getLastProcessedRow() >= 0) {
                            odfRow = sheet.getOooSheet().getRowByIndex(processResult.getLastProcessedRow() + 1);
                        } else {
                        	odfRow = sheet.getOooSheet().getRowByIndex(0);
                        }
                    } else
                    	odfRow = SheetHelper.getNextRow(odfRow);
                    // process other transformers
                    for (int j = 1; j < rowTransformers.size(); j++) {
                        rowTransformer = (RowTransformer) rowTransformers.get(j);
                        if (rowTransformer != null) {
                            ResultTransformation newTransformation = rowTransformer.transform(stc, this/*, processResult*/);
                            processResult.add(newTransformation);
                            ownTransformers.add(rowTransformer);
                        }
                    }
                } else
                	odfRow = SheetHelper.getNextRow(odfRow);
            }
            groupRows(sheet);
        }
//        sheet.setBeanParams(null);
    }

    private void exposePOIBeans(Sheet sheet) {
        sheet.getBeanParams().put(configuration.getSheetKeyName(), sheet.getOooSheet());
    }


    /**
     * Processes rows in a template sheet using map of beans as parameter
     *
     * @param stc       - {@link SheetTransformationController} corresponding to the sheet containing given rows
     * @param sheet     {@link org.odst.transformer.Sheet} object
     * @param startRow  Row to start processing
     * @param endRow    Last row to be processed
     * @param beans     Beans for substitution
     * @param parentRow - {@link Row} object representing original template row linked to rows to process
     * @return A number of rows to be shifted
     * @throws ParsePropertyException
     */
    public ResultTransformation processRows(SheetTransformationController stc, int startRow, int endRow, Row parentRow) throws ParsePropertyException {
        int origEndRow = endRow;
        int nextRowShiftNumber = 0;
        boolean hasTagProcessing = false;
        int lastProcessedRow = -1;
        int count = 0;
        for (int i = startRow; i <= endRow; i++) {
                ResultTransformation processResult = processRow(stc, stc.getSheet().getOooSheet().getRowByIndex(i), parentRow);
                if (!processResult.isTagProcessResult()) {
                    int shiftNumber = processResult.getNextRowShift();
                    nextRowShiftNumber += shiftNumber;
                    endRow += processResult.getLastRowShift();
                    i += shiftNumber;
                    lastProcessedRow = i;
                } else {
                    hasTagProcessing = true;
                    if (processResult.getLastProcessedRow() >= 0) {
                        i = processResult.getLastProcessedRow();
                        lastProcessedRow = i;
                    } else {
                        i--;
                    }
                    endRow += processResult.getLastRowShift();
                }
                count++;
        }
        ResultTransformation r = new ResultTransformation(nextRowShiftNumber, endRow - origEndRow);
        r.setTagProcessResult(hasTagProcessing);
        r.setLastProcessedRow(lastProcessedRow);
        return r;
    }

    ResultTransformation processRow(SheetTransformationController stc, OdfTableRow hssfRow, Row parentRow) {
        return processRow(stc, hssfRow, 0, -1, parentRow);
    }

    public ResultTransformation processRow(SheetTransformationController stc, OdfTableRow hssfRow, int startCell, int endCell, Row parentRow) {
    	List<RowTransformer> transformers = parseCells(stc.getSheet(), hssfRow, startCell, endCell);
    	ChainTransformer chainTransformer = new ChainTransformer(transformers, stc.getSheet(), rowProcessors, parentRow);
        ResultTransformation result = chainTransformer.transform(stc, this);
        return result;
    }

    private List<RowTransformer> parseCells(Sheet sheet, OdfTableRow hssfRow, int startCell, int endCell) {
        if (configuration.getRowKeyName() != null) {
            sheet.getBeanParams().put(configuration.getRowKeyName(), hssfRow);
        }

        List<RowTransformer> transformers = new ArrayList<RowTransformer>();
        RowTransformer rowTransformer = null;
        Row row = new Row(sheet, hssfRow);
        SimpleRowTransformer simpleRowTransformer = new SimpleRowTransformer(row, cellProcessors, configuration);
//        transformations.add( simpleRowTransformer );
        boolean hasCollections = false;
        OdfTableCell odfCell = hssfRow.getCellByIndex(startCell);
        while(odfCell != null && (endCell >= startCell ? odfCell.getColumnIndex() <= endCell : true)) {
            CellParser cellParser = new CellParser(odfCell, row, configuration);
            Cell cell = cellParser.parseCell(sheet.getBeanParams());
            if (cell.getTag() == null) {
                if (cell.getLabel() != null && cell.getLabel().length() > 0) {
                    sheet.addNamedCell(cell.getLabel(), cell);
                }
                RowCollection rowCollection = row.addCell(cell);
                if (cell.getCollectionProperty() != null) {
                    hasCollections = true;
                    if (rowTransformer == null) {
                        rowTransformer = new CollectionRowTransformer(row, fixedSizeCollections, cellProcessors, rowProcessors, configuration);
                        transformers.add(rowTransformer);

                    }
                    ((CollectionRowTransformer) rowTransformer).addRowCollection(rowCollection);

//                    rowTransformer

                    ListRange listRange = new ListRange(row.getRowIndex(), row.getRowIndex() + rowCollection.getCollectionProperty().getCollection().size() - 1, odfCell.getColumnIndex());

                    addListRange(sheet, cell.getCollectionProperty().getProperty(), listRange);
                } else {
                    if (!cell.isEmpty()) {
                        simpleRowTransformer.addCell(cell);
                    }
                }
                odfCell = SheetHelper.getNextCell(odfCell);
            } else {
                rowTransformer = new TagRowTransformer(row, cell);
                Block tagBody = cell.getTag().getTagContext().getTagBody();
                odfCell = hssfRow.getCellByIndex(odfCell.getColumnIndex() + tagBody.getNumberOfColumns());
                transformers.add(rowTransformer);
//                break;
            }
        }
        if (!hasCollections && simpleRowTransformer.getCells().size() > 0) {
            transformers.add(simpleRowTransformer);
        }

        // update references to parent RowCollections and process formula cells
        for (int i = 0; i < row.getCells().size(); i++) {
            Cell cell = (Cell) row.getCells().get(i);
            if (cell.getTag() == null) {
                if (cell.getRowCollection() == null && cell.getCollectionName() != null) {
                    RowCollection rowCollection = row.getRowCollectionByCollectionName(cell.getCollectionName());
                    if (rowCollection != null) {
                        rowCollection.addCell(cell);
                    } else {
                        log.debug("RowCollection with name " + cell.getCollectionName() + " not found");
                    }
                } else {
                    // add null cells to all hssfRow collections
                    if (cell.isEmpty() && cell.getRowCollection() == null && cell.getMergedRegion() == null && row.getRowCollections().size() == 1) {
                        ((RowCollection) row.getRowCollections().get(0)).addCell(cell);
                    }
                }
                // process formula cell
                if (cell.isFormula()) {
                    // create list range for inline formula
                    if (cell.getFormula().isInline() && cell.getLabel() != null && cell.getLabel().length() > 0) {
                        ListRange listRange = new ListRange(row.getOooRow().getRowIndex(),
                                row.getOooRow().getRowIndex() + cell.getRowCollection().getCollectionProperty().getCollection().size() - 1,
                                cell.getOooCell().getColumnIndex());
                        addListRange(sheet, cell.getLabel(), listRange);
                    }
                }
            }
        }
        return transformers;
    }


    List<RowTransformer> parseRow(Sheet sheet, OdfTableRow hssfRow) {
        List<RowTransformer> transformers = parseCells(sheet, hssfRow, 0, -1);
        return transformers;
    }


    /**
     * Adds new {@link org.odst.formula.ListRange} to the map of ranges and updates formulas if there is range with the same name already
     *
     * @param sheet     - {@link Sheet} to add List Range
     * @param rangeName - The name of {@link ListRange} to add
     * @param range     - actual {@link ListRange} to add
     * @return true     - if a range with such name already exists or false if not
     */
    private boolean addListRange(Sheet sheet, String rangeName, ListRange range) {
        if (sheet.getListRanges().containsKey(rangeName)) {
            // update all formulas that can be updated and remove them from formulas list ( ignore all others )
            sheet.addListRange(rangeName, range);
            return true;
        }
        sheet.addListRange(rangeName, range);
        return false;
    }

    /**
     * Applies all registered RowProcessors to a row
     *
     * @param sheet - {@link Sheet} containing given {@link Row} object
     * @param row   - {@link org.odst.transformer.Row} object with row information
     */
    private void applyRowProcessors(Sheet sheet, Row row) {
        for (RowProcessor rowProcessor : rowProcessors) {
            rowProcessor.processRow(row, sheet.getNamedCells());
        }
    }

    /**
     * Outlines all required collections in a sheet
     *
     * @param sheet - {@link Sheet} where to outline collections
     */
    void groupRows(Sheet sheet) {
        for (String collectionName : groupedCollections) {
            if (sheet.getListRanges().containsKey(collectionName)) {
                ListRange listRange = (ListRange) sheet.getListRanges().get(collectionName);
                OdfTableRow firstRow = sheet.getOooSheet().getRowByIndex(listRange.getFirstRowNum());
                Node tableRowGroup = sheet.getOooSheet().getOdfElement().getOwnerDocument().createElementNS(OdfNamespaceNames.TABLE.getUri(), "table:table-row-group");
                sheet.getOooSheet().getOdfElement().insertBefore(tableRowGroup, firstRow.getOdfElement());
                OdfTableRow row = firstRow;
                for (int i=listRange.getFirstRowNum();i<=listRange.getLastRowNum();i++) {
                	OdfTableRow nextRow = row.getNextRow();
                	tableRowGroup.appendChild(sheet.getOooSheet().getOdfElement().removeChild(row.getOdfElement()));
                	row = nextRow;
                }
            }
        }
    }


}
